# server.py
import time
import socket
import logging  # 日志
import threading  # 多线程
import psycopg2  # 数据库
import psycopg2.extras
import psycopg2.errors
import pymysql
import pymysql.cursors
import configparser  # config读取
import random
import atexit
import os.path
from maps import *  # 导入地图
from rich.traceback import install
from rich.logging import RichHandler
from rich.console import Console
from schedule import every, repeat, run_pending


class Data:
    def __init__(self, conn) -> None:
        self.conn = conn
        self.AccountData = {}
        # {account_name:{"uuid":...,"password":...},...}
        self.CharacterData = {}
        # {uuid:{1:"name",2:...},...}
        self.CharacterAttribute = {}
        # {(uuid,1):{"hp":...,...},...}
        self.CharacterLocation = {}
        # {(uuid,1):{"position":[x,y],"dimension":...},...}
        self.NewCharacterData = {}
        # {uuid:{id:"name"},...}
        self.NewAccountData = {}
        # {account_name:{"uuid":...,"password":...},...}

    def getSQLAccountData(self) -> dict:
        '''
        返回的dict样式：{用户名:{"uuid":xxx, "password":xxx}}
        此处MySQL与PostgreSQL逻辑相同，无需更改
        '''
        result = self.executeSQL("SELECT uuid,account,password FROM users")
        account = {}
        for i in range(len(result)):
            dict_result = dict(result[i])  # 将result化为字典（为了兼容psycopg2）
            account[dict_result['account']] = {
                'uuid': dict_result['uuid'],
                'password': dict_result['password'],
            }
            # {用户名:{"uuid":xxx, "password":xxx}}
        return account

    def getSQLCharacterData(self) -> dict:
        '''
        返回的dict样式：{UUID:{角色ID:角色名, 角色ID:角色名,......}}
        此处MySQL需要加``号
        '''
        character = {}
        if Settings["dbType"] == "PostgreSQL":
            result = self.executeSQL(
                "SELECT UUID,character_id,character_name FROM character"
            )
        else:
            result = self.executeSQL(
                "SELECT UUID,character_id,character_name FROM `character`"
            )
        for i in range(len(result)):
            dict_result = dict(result[i])
            try:
                if dict_result['UUID'] not in character:  # 第一个角色
                    character[dict_result['UUID']] = {
                        dict_result['character_id']: dict_result['character_name']
                    }
                    # {UUID:{角色ID:角色名, 角色ID:角色名,......}}
                    continue
                character[dict_result['UUID']][
                    dict_result['character_id']
                ] = dict_result[
                    'character_name'
                ]  # 第一个之后的
            except KeyError:  # 有时为小写，视数据库而定
                if dict_result['uuid'] not in character:  # 第一个角色
                    character[dict_result['uuid']] = {
                        dict_result['character_id']: dict_result['character_name']
                    }
                    # {UUID:{角色ID:角色名, 角色ID:角色名,......}}
                    continue
                character[dict_result['uuid']][
                    dict_result['character_id']
                ] = dict_result[
                    'character_name'
                ]  # 第一个之后的

        return character

    def getSQLCharacterAttribute(self) -> dict:
        '''
        返回的dict样式：{(UUID,角色ID):{'hp':xxx,'hp_max':xxx,...}}
        此处MySQL与PostgreSQL逻辑相同，无需更改
        '''
        result = self.executeSQL("SELECT * FROM attribute")
        attribute = {}
        for i in range(len(result)):
            dict_result = dict(result[i])
            attribute[(dict_result['uuid'], dict_result['character_id'])] = {
                'hp_max': dict_result['hp_max'],
                'hp': dict_result['hp'],
                'atk': dict_result['atk'],
                'def': dict_result['def'],
                'crit': dict_result['crit'],
                'crit_damage': dict_result['crit_damage'],
                'dex': dict_result['dex'],
                'pot': dict_result['pot'],
                'lv': dict_result['lv'],
                'exp': dict_result['exp'],
                'exp_max': dict_result['exp_max'],
                'coin': dict_result['coin'],
            }

        return attribute

    def getSQLCharacterPosition(self):
        '''
        返回的dict样式: {(UUID,角色ID):{"position":[x,y], "dimension":"xxx"}}
        此处MySQL与PostgreSQL逻辑相同，无需更改
        '''
        result = self.executeSQL("SELECT * FROM position")
        location = {}
        for i in range(len(result)):
            dict_result = dict(result[i])
            location[(dict_result['uuid'], dict_result['character_id'])] = {
                "dimension": dict_result['dimension'],
                "position": [dict_result["x"], dict_result["y"]],
            }
        return location

    def executeSQL(self, command: str):
        # 执行command并返回结果
        if Settings["dbType"] == "PostgreSQL":
            with self.conn.cursor(
                cursor_factory=psycopg2.extras.RealDictCursor
            ) as cursor:
                cursor.execute(command)
                result = cursor.fetchall()
        else:
            with self.conn.cursor() as cursor:
                cursor.execute(command)
                result = cursor.fetchall()
        return result

    def setup(self):
        self.AccountData = self.getSQLAccountData()
        self.CharacterData = self.getSQLCharacterData()
        self.CharacterAttribute = self.getSQLCharacterAttribute()
        self.CharacterLocation = self.getSQLCharacterPosition()

    def setAttribute(self, addr, attribute, value=None, delta=None):
        uuid = self.getUUID(addr)
        character_id = self.getCharacterID(addr)
        if value and not delta:
            self.CharacterAttribute[(uuid, character_id)][attribute] = value
        elif delta and not value:
            if (
                attribute in ["hp", "exp"]
                and self.CharacterAttribute[(uuid, character_id)][attribute] + delta
                > self.CharacterAttribute[(uuid, character_id)][f"{attribute}_max"]
            ):  # 若增加属性后大于属性最大值
                self.CharacterAttribute[(uuid, character_id)][
                    attribute
                ] = self.CharacterAttribute[(uuid, character_id)][
                    f"{attribute}_max"
                ]  # 设置为最大值
            else:
                self.CharacterAttribute[(uuid, character_id)][attribute] += delta

    def setPosition(self, addr, position):
        uuid = users[addr]["uuid"]  # 获取UUID
        characterID = users[addr]["characterID"]  # 获取角色ID
        self.CharacterLocation[(uuid, characterID)]["position"] = position

    def setDimension(self, addr, dimension="目标维度"):
        uuid = users[addr]["uuid"]  # 获取UUID
        characterID = users[addr]["characterID"]  # 获取角色ID
        self.CharacterLocation[(uuid, characterID)]["dimension"] = dimension

    def addNewCharacter(self, addr, id=None, name=None):
        uuid = self.getUUID(addr)
        self.NewCharacterData[uuid] = {}
        self.CharacterData[uuid] = {}
        if id and name:
            self.NewCharacterData[uuid][id] = name
            self.CharacterData[uuid][id] = name

    def addNewCharacterAttribute(self, uuid, id):
        self.CharacterAttribute[(uuid, id)] = {
            'hp_max': 200,
            'hp': 200,
            'atk': 10,
            'def': 10,
            'crit': 1,
            'crit_damage': 50,
            'dex': 90,
            'pot': 0,
            'lv': 1,
            'exp': 0,
            'exp_max': 10,
            'coin': 500,
        }

    def addNewCharacterPosition(self, uuid, id):
        self.CharacterLocation[(uuid, id)] = {
            "position": eval(Settings["default_position"]),  # 设置坐标为默认坐标
            "dimension": f"{Settings['default_dimension']}",  # 设置维度为默认维度
        }

    def addNewAccount(self, uuid, name, password):
        self.NewAccountData[name] = {
            "uuid": uuid,
            "password": password,
        }  # 加入到新增用户
        self.AccountData[name] = {
            "uuid": uuid,
            "password": password,
        }  # 加入到新增用户

    def getAccountName(self, addr):
        return users[addr]["account"]

    def getNewAccountUUID(self, addr=None, account=None):
        if addr and not account:
            account = self.getAccountName(addr)  # 若只输入addr，则获取name
        elif not addr and not account:
            return None  # 既不输入addr也不输入name（您介似奏嘛啊？）
        try:
            return self.NewAccountData[account]["uuid"]
        except KeyError:
            return None

    def getNewAccountPassword(self, addr=None, account=None):
        if addr and not account:
            account = self.getAccountName(addr)  # 若只输入addr，则获取name
        elif not addr and not account:
            return None  # 既不输入addr也不输入name（您介似奏嘛啊？）
        try:
            return self.NewAccountData[account]["password"]
        except KeyError:
            return None

    def getCharacters(self, addr):
        """
        获取该uuid下的所有角色

        Returns
        -------
        dict
            {1:name,2:name,...}
        """
        return self.CharacterData[self.getUUID(addr)]

    def getCharacterName(self, addr, id: int = 0):
        if not id:
            return users[addr]["characterName"]
        uuid = self.getUUID(addr)
        return self.CharacterData[uuid][id]

    def getCharacterID(self, addr):
        return users[addr]["characterID"]

    def getUUID(self, addr):
        return users[addr]["uuid"]

    def getPosition(
        self, addr=None, uuid_characterID: tuple = None
    ):  # sourcery skip: class-extract-method
        if addr and not uuid_characterID:  # 使用addr获取坐标
            uuid = users[addr]["uuid"]  # 获取UUID
            characterID = users[addr]["characterID"]  # 获取角色ID
            return self.CharacterLocation[(uuid, characterID)]["position"]
        elif uuid_characterID and not addr:  # 使用uuid_charaterID获取坐标
            return self.CharacterLocation[uuid_characterID]["position"]
        else:
            return None

    def getLocation(
        self, addr=None, uuid_characterID: tuple = None
    ):  # sourcery skip: class-extract-method
        if addr and not uuid_characterID:  # 使用addr获取位置
            uuid = users[addr]["uuid"]  # 获取UUID
            characterID = users[addr]["characterID"]  # 获取角色ID
            return self.CharacterLocation[(uuid, characterID)]
        elif uuid_characterID and not addr:  # 使用uuid_charaterID获取位置
            return self.CharacterLocation[uuid_characterID]
        else:
            return None

    def getDimension(self, addr):
        uuid = users[addr]["uuid"]  # 获取UUID
        characterID = users[addr]["characterID"]  # 获取角色ID
        return self.CharacterLocation[(uuid, characterID)]["dimension"]

    def getAllCharacterAttribute(self):
        return self.CharacterAttribute

    def getAllAccountData(self):
        return self.AccountData

    def getAllNewAccountData(self):
        return self.NewAccountData

    def getAllNewCharacterData(self):
        """
        获取全部新增角色数据

        Returns
        -------
        dict
            {UUID:{id:name},...}
        """
        return self.NewCharacterData

    def getAllCharacterData(self):
        """
        获取全部角色数据

        Returns
        -------
        dict
            {uuid:{1:name,2:name},...}
        """
        return self.CharacterData

    def getAllCharacterLocation(self):
        return self.CharacterLocation

    def getCharacterAttribute(self, addr=None, uuid_characterId=None):
        if addr and not uuid_characterId:
            uuid = users[addr]["uuid"]  # 获取UUID
            characterID = users[addr]["characterID"]  # 获取角色ID
        elif not addr and not uuid_characterId:
            return None
        else:
            uuid = uuid_characterId[0]
            characterID = uuid_characterId[1]
        return self.CharacterAttribute[(uuid, characterID)]

    def getUUIDList(self):
        return list(self.CharacterData.keys())


def shutDown(sock, dbconn):  # sourcery skip: instance-method-first-arg-name
    sock.close()
    dbconn.close()
    logger.info("数据库和socket连接已自动退出")


def probabilityFunc(rate) -> bool:
    '''
    概率函数
    参数rate为int，即为True的概率
    返回True/False
    '''
    random_number = random.randint(1, 100)  # 随机在1~100间抽取一个数
    return random_number <= rate


def isAllChinese(string) -> bool:
    '''
    判断是否全部为中文
    '''
    return all('\u4e00' <= word <= '\u9fa5' for word in string)


def connectDB():
    '''
    创建数据库连接，返回一个connect对象
    '''
    try:
        if Settings["dbType"] == "PostgreSQL":
            conn = psycopg2.connect(
                host=Settings["dbHost"],
                port=Settings["dbPort"],
                user=Settings["dbUser"],
                password=Settings["dbPassword"],
                database=Settings["Db"],
            )  # 连接数据库
        else:
            conn = pymysql.connect(
                host=Settings["dbHost"],
                port=Settings["dbPort"],
                user=Settings["dbUser"],
                password=Settings["dbPassword"],
                database=Settings["Db"],
                cursorclass=pymysql.cursors.DictCursor,
            )
    except Exception:
        logger.error("数据库连接失败！")
        exit()
    return conn  # 返回连接对象


def closeDB(conn):
    '''
    关闭数据库
    注：数据库需关闭才能提交修改内容
    '''
    conn.commit()  # 提交
    conn.close()  # 关闭


def getConfig(section, key: str = None) -> str:
    '''
    获取[section]下[key]对应的值
    '''
    config = configparser.ConfigParser()
    config.read(f"{path}\config.ini", encoding='utf-8')  # 读取config
    if key != None:  # 指定了key
        return config.get(section, key)
    else:  # 未指定key（读取section下的所有key）
        return config.items(section)


def writeConfig(section: str, key: list, value: list) -> None:
    '''
    将[section]下[key]的值定为[value]
    若只填写section，则创建[section]
    [key] -> list
    [value] -> list
    '''
    config = configparser.ConfigParser()  # 指定config读取器
    config.read(f'{path}\config.ini', encoding="utf-8")

    if not config.has_section(section):
        config.add_section(section)
    for i in range(len(key)):  # 依次对应填写
        config.set(section, str(key[i]), str(value[i]))  # 设置值
    config.write(open(f"{path}\config.ini", "w"))


def serverOnline(user: dict) -> list:
    '''
    获取在线账户名
    '''
    user_list = list(user.values())
    return [user_["account"] for user_ in user_list]


def accountDataUpdate():
    # 账户数据上传
    conn = connectDB()  # 连接数据库
    cursor = conn.cursor()
    for account in data.getAllNewAccountData():  # 在accountsDataNew中提取出账户名
        command = """INSERT INTO users (uuid,account,password) 
                    VALUES ({UUID},'{Account}','{Password}')""".format(
            UUID=data.getNewAccountUUID(account=account),
            Account=account,
            Password=data.getNewAccountPassword(account=account),
        )  # 插入到数据库中

        cursor.execute(command)
    cursor.close()
    closeDB(conn)
    logger.info("新账户数据上传完毕")


def characterAttributesUpdate():
    # 角色属性上传
    conn = connectDB()
    for uuid_characterID in data.getAllCharacterAttribute():
        attributes = data.getCharacterAttribute(uuid_characterId=uuid_characterID)
        command = """UPDATE attribute SET 
                hp_max={hp_max},hp={hp},          atk={atk},
                def={Def},      crit={crit},      crit_damage={crit_damage},
                dex={dex},      pot={pot},        lv={lv},
                exp={exp},      exp_max={exp_max},coin={coin} 
                WHERE uuid={uuid} AND character_id={characterID}""".format(
            hp_max=attributes["hp_max"],
            hp=attributes["hp"],
            atk=attributes["atk"],
            Def=attributes["def"],
            crit=attributes["crit"],
            crit_damage=attributes["crit_damage"],
            dex=attributes["dex"],
            pot=attributes["pot"],
            lv=attributes["lv"],
            exp=attributes["exp"],
            exp_max=attributes["exp_max"],
            coin=attributes["coin"],
            uuid=uuid_characterID[0],
            characterID=uuid_characterID[1],
        )  # 壮  观
        cursor = conn.cursor()
        cursor.execute(command)
        cursor.close()
    closeDB(conn)
    logger.info("角色属性上传完毕")


def locaionUpdate():
    # 位置上传
    conn = connectDB()
    for uuid_characterID in data.getAllCharacterLocation():
        character_location = data.getLocation(uuid_characterID=uuid_characterID)
        command = """UPDATE position SET 
        dimension='{dimension}', x={x}, y={y}
        WHERE uuid={uuid} AND character_id={character_id}""".format(
            dimension=character_location["dimension"],
            x=character_location["position"][0],
            y=character_location["position"][1],
            uuid=uuid_characterID[0],
            character_id=uuid_characterID[1],
        )
        cursor = conn.cursor()
        cursor.execute(command)
        cursor.close()
    closeDB(conn)
    logger.info("角色位置上传完毕")


def characterDataUpdate():
    """
    新角色信息上传
    注：由于是新角色，表内并没有对应的UUID&character_id，所以需要预先在各表中插入
    """
    conn = connectDB()  # 连接数据库
    character_data_new = data.getAllNewCharacterData()
    for UUID in character_data_new:  # 从新增角色中提取出UUID
        cursor = conn.cursor()
        CharacterID = list(character_data_new[UUID].keys())[0]
        CharacterName = list(character_data_new[UUID].values())[0]
        if Settings["dbType"] == "PostgreSQL":
            command = f"""INSERT INTO character (uuid,character_id,character_name) VALUES ({UUID},{CharacterID},'{CharacterName}')"""  # 插入到character(PostgreSQL)
        else:
            command = f"""INSERT INTO `character` (uuid,character_id,character_name) VALUES ({UUID},{CharacterID},'{CharacterName}')"""  # 插入到character(MySQL)
        cursor.execute(command)

        command = f"""INSERT INTO attribute (uuid,character_id) VALUES ({UUID},{CharacterID})"""  # 插入到attribute

        cursor.execute(command)

        default_position = eval(Settings["default_position"])
        command = """INSERT INTO position (uuid,character_id,dimension,x,y) VALUES ({UUID},{CharacterID},'{dimension}',{x},{y})""".format(
            UUID=UUID,
            CharacterID=CharacterID,
            dimension=Settings["default_dimension"],
            x=default_position[0],
            y=default_position[1],
        )
        # 注：为了适配MySQL，此处 不   能   换   行 （大悲
        cursor.execute(command)  # 插入到Position
        cursor = conn.cursor()
        cursor.close()
    closeDB(conn)
    logger.info("新建角色数据初始化完毕")


def showOnline():
    logger.info("当前在线人员有：")
    user_values = list(users.values())
    for user_value in user_values:
        print("- " + user_value["account"])
    print("     共{Number}人".format(Number=len(user_values)))


def commandExit(addr):
    Account = data.getAccountName(addr)
    logger.info(f"[yellow]<{Account}>退出了游戏[/]", extra={"markup": True})
    name = data.getCharacterName(addr)  # 提取出名字
    del users[addr]
    for address in users:
        sock.sendto("202{Name}".format(Name=name).encode(), address)  # 广播xxx退游戏啦！
    sock.sendto("299".encode('utf-8'), addr)  # 向该玩家发送exit（可退出）


def commandMove(addr, message):  # sourcery skip: extract-method
    command, direction = message.split(" ", 1)
    # 只分割一次是为了防止有哪个神经病写出了"go   "(一堆空格)这样的指令
    dimension = data.getDimension(addr)  # 获取维度
    position = data.getPosition(addr)  # 获取位置
    try:
        command = (
            f"{dimension}.world.moveTo({position},'{direction.upper()}')"  # 生成移动指令
        )
        position_moved = eval(command)  # 获取移动后坐标
        command = f"{dimension}.world.getCurrPlace({position_moved})"
        moved_place = eval(command)  # 获取移动后地点
        if moved_place.getPortal() == True:
            # 如果移动后地点是个传送门，就传送到目的地
            data.setDimension(addr, moved_place.getDestination())
            data.setPosition(addr, moved_place.getDestinationPosition())
        else:
            data.setPosition(addr, position_moved)
        commandMap(addr)  # 获取地图
        commandLook(addr)  # 获取当前位置信息
    except map_temp.MapEdgeError:
        curr_place = eval(f"{dimension}.world.getCurrPlace({position})")
        if curr_place.getPortal() == True:
            data.setDimension(addr, curr_place.getDestination())
            data.setPosition(addr, curr_place.getDestinationPosition())
            commandMap(addr)  # 获取地图
            commandLook(addr)  # 获取当前位置信息
        else:
            sock.sendto("106".encode("utf-8"), addr)  # 抵达地图边界

    except map_temp.DirectionError or KeyError:
        sock.sendto("107".encode("utf-8"), addr)  # 指令参数错误


def commandHp(addr):
    logger.info(f"<{data.getAccountName(addr)}>获取了用户信息")
    sock.sendto(
        f"204{data.getCharacterAttribute(addr)}".encode("utf-8"), addr
    )  # 发送玩家状态："204{"hp":xxx,"atk":xxx,......}"


def commandSay(addr, message):
    try:
        _, text = message.split(" ", 1)
    except ValueError:
        sock.sendto("109".encode("utf-8"), addr)
        return
    logger.info(f"<{data.getAccountName(addr)}> 说:{text} (say)")
    CharacterName = data.getCharacterName(addr)
    for address in users:  # 从user遍历出address
        sock.sendto(
            f"201{CharacterName}|{text}".encode('utf-8'),
            address,
        )  # 发送所说信息


def commandMap(addr, sizable=True):
    '''
    获取当前地图
    '''
    dimension = data.getDimension(addr)  # 获取维度
    pos = data.getPosition(addr)  # 获得当前位置
    account = data.getAccountName(addr)  # 获取账户名
    if sizable:
        size = int(Settings["size"])  # 获取范围大小
        command = f"{dimension}.world.getStringMap(pos={pos}, size={size})"  # 生成指令
    else:
        command = f"{dimension}.world.getStringMap(pos={pos})"  # 生成指令
    worldmap_str = eval(command)  # 获取地图
    logger.debug(f"<{account}> 在 {pos} 获取了地图（map）")
    sock.sendto(f"205{worldmap_str}|{pos}".encode("utf-8"), addr)  # 发送


def commandLook(addr, message="look"):
    '''
    获取当前地点指令
    '''
    dimension = data.getDimension(addr)  # 获取维度
    pos = data.getPosition(addr)  # 获取位置
    Account = data.getAccountName(addr)  # 获取账户名
    command = f"{dimension}.world.getCurrPlace({pos})"  # 生成指令
    curr_place = eval(command)  # 获取当前位置实例

    if message == "look":  # 如果指令只有“look”（查看该地点）
        name = curr_place.getName()  # 获取名称

        info = curr_place.getInfo()  # 获取信息
        command = f"{dimension}.world.getIfExit({pos})"  # 生成获取if_exit的指令
        if_exit = eval(command)  # 获取if_exit

        command = f"{dimension}.world.getStrExitUnits({pos})"  # 生成获取出口对象的指令
        exit_strs = eval(command)  # 获取出口对象

        command = f"{dimension}.world.getCurrStrUnits({pos})"  # 生成获取当前地点对象的指令
        units = eval(command)  # 获取该地点单位

        logger.debug(f"<{Account}> 在 {pos} 获取了位置信息（look）")
        sock.sendto(
            f"206{name}|{info}|{if_exit}|{exit_strs}|{units}".encode("utf-8"),
            addr,
        )  # 全发送咯

    elif " " in message:  # 若指令不只有look
        command, item = message.split(" ", 1)
        units = eval(f"{dimension}.world.getCurrDictUnits({pos})")
        # units = {名称：实例, ...}
        if item in units:
            unit = units[item]
            name = item
            info = unit.getInfo()
            actions = unit.getActions()
            amount = unit.amount
            logger.info(f"<{Account}> 在 {pos} 获取了 {item} 的信息（look）")
            sock.sendto(
                f"207{name}|{info}|{actions}|{amount}".encode("utf-8"), addr
            )  # 发送单位信息

        else:
            sock.sendto(f"108{item}", addr)  # 错误码：该单位不存在


def commandAction(addr, message):
    """
    执行玩家对物体的操作
    流程为：
        1. server调用execute找到对应func
        2. execute无参数调用func，若返回值为字典(requirements)，则在返回的列表中标注为“requirements”，
           若返回值为其他类型，则标注为“normal”。
        3. 若server传入了参数，就将该参数传递给func，返回结果标注“normal”

    参数
    ----------
    addr : tuple
        该玩家IP
    message : str
        该玩家发送的指令
    """
    try:
        _, action, action_unit = message.split(" ", 2)
    except ValueError:
        sock.sendto("109".encode("utf-8"), addr)
    dimension = data.getDimension(addr)
    pos = data.getPosition(addr)
    dict_units = eval(f"{dimension}.world.getCurrDictUnits({pos})")  # 获取当前地点单位
    unit = dict_units[action_unit]  # 获取命令指定的单位
    # ↑ 获取一系列变量
    returns = unit.execute(action=action)  # 无参数运行
    if returns[0] == "normal":  # 获取值 [style, results]
        returns = returns[1]
    elif returns[0] == "requirements":
        requirements = returns[1]  # {"名称":"变量名"(str)}
        args = {}
        for key in requirements:
            true_value = eval(requirements[key])  # 获取变量名下的值
            args[key] = true_value  # {"名称":真实值(any)}
        returns = unit.execute(
            action=action, args=args
        )  # 有参数运行,[style1, [(style,result),...]]
        """
        注：有参数运行后，
        returns=[
                normal,
                    [
                    (command/...,
                    "...")
                    ]
                ]
        """
        returns = returns[1]  # [(style, result),...]
    logger.debug(
        f"<{data.getAccountName(addr)}> 在 {pos} 对 {action_unit} 执行了 {action} 指令"
    )
    for item in returns:  # item (style, returns)
        if item[0] == "command":
            exec(item[1])  # 执行指令
        else:
            sock.sendto(
                "{style}{message}".format(style=item[0], message=item[1]).encode(
                    "utf-8"
                ),
                addr,
            )


def update():
    # 退出处理（上传数据至数据库）

    locaionUpdate()
    characterAttributesUpdate()

    if data.NewAccountData:  # 若有新账户
        # 新账户上传
        accountDataUpdate()

    if data.NewCharacterData:  # 若有新角色
        # 新角色上传
        characterDataUpdate()

    # 角色属性上传（不管更新没更新）

    logger.info("所有数据均已上传完毕")


@repeat(every(10).minutes)
def auto_update():
    # 退出处理（上传数据至数据库）
    start = time.time()
    locaionUpdate()
    characterAttributesUpdate()

    if data.NewAccountData:  # 若有新账户
        # 新账户上传
        accountDataUpdate()

    if data.NewCharacterData:  # 若有新角色
        # 新角色上传
        characterDataUpdate()

    # 角色属性上传（不管更新没更新）
    end = time.time()
    cost_time = end - start

    logger.info(
        f"[yellow]<自动保存> 所有数据均已上传完毕 耗时{cost_time*1000:.0f}ms[/]", extra={"markup": True}
    )


def auto_functions():
    try:
        while True:
            run_pending()
    except Exception:
        logger.exception(":warning-emoji:  错误！", extra={"markup": True})


def Server_Input():
    '''
    指令输入函数
    '''

    while True:
        try:
            command = input("")  # 获取输入内容

            if "map" in command:
                # 获取地图（输出）
                command, map_ = command.split(" ")  # 以空格为分界线，提取指令参数map_
                command_map = f"{map_}.world.formatWorldMap()"  # 执行
                print(eval(command_map))

            elif command == "exit":
                # 上传数据并退出
                update()
                logger.info("3s后自动关闭")
                time.sleep(3)
                exit()

            elif command == "update":
                # 上传数据
                logger.info("正在上传数据...")
                update()

            elif command == "exit -n":
                # 不上传数据退出
                exit()

            elif command.lower() == "online":
                # 获取线上用户
                showOnline()
            elif "show " in command:
                _, var = command.split(" ", 1)
                缩写字典 = {
                    "ca": "data.getAllCharacterAttribute()",
                    "cp": "data.getAllCharacterLocation()",
                    "cdn": "data.getAllNewCharacterData()",
                    "adn": "data.getAllNewAccountData()",
                    "ad": "data.getAllAccountData()",
                    "cd": "data.getAllCharacterData()",
                }
                for 缩写 in 缩写字典:
                    var = var.replace(缩写, 缩写字典[缩写])
                console.print(eval(var))
            else:
                exec(command)
        except Exception:
            logger.exception(":warning-emoji:  错误！", extra={"markup": True})


def Client_Input():  # sourcery skip: extract-duplicate-method
    '''
    主输出函数
    '''

    while True:
        try:
            data, addr = sock.recvfrom(4096)  # 等待接收客户端消息存放在2个变量data和addr里
            message = data.decode('utf-8')
            # t1 = time.time()
            if addr not in users:  # 如果addr不在user字典里则执行以下代码
                # 判断是否有账号
                if message in ["exit", ""]:
                    logger.warning(f"有人以'{message}'为名称登录，已屏蔽。地址:[{addr[0]}:{addr[1]}]")
                    continue
                newUser(addr, message)
                continue

            elif users[addr]["status"] == "login":  # 该IP处于登录状态
                # 登录
                newlogin(addr, message)  # 发送角色列表："008[1,2,...]|["xxx","xx",...]"
                continue

            elif users[addr]["status"] == "reg":  # 该IP处于注册状态
                """
                注：此处MySQL与PostgreSQL逻辑不同
                """
                newReg(addr, message)
                continue

            elif users[addr]["status"] == "createCharacter":
                # 创建角色
                newCreateCharacter(addr, message)
                continue

            elif users[addr]["status"] == "choseCharacter":
                # 选择角色
                newChoseCharacter(addr, message)
                continue

            elif addr not in users and message in serverOnline(users):
                # 若该账号已经登陆，但addr不在user中
                sock.sendto("103".encode('utf-8'), addr)  # 重复登陆错误码
                continue

            elif "say" in message:
                # 对其它玩家说话
                commandSay(addr, message)
                continue

            elif message == "hp":
                # 玩家获取当前状态
                commandHp(addr)
                continue

            elif message == "map":
                # 玩家获取当前地图
                commandMap(addr, sizable=True)
                continue

            elif message == "map all":
                commandMap(addr, sizable=False)
                continue

            elif "look" in message:
                commandLook(addr, message)
                continue

            elif "go" in message:
                # 玩家移动
                commandMove(addr, message)
                continue

            elif "action" in message:
                commandAction(addr, message)

            elif 'EXIT'.lower() in message:  # 如果EXIT在发送的data里
                # 玩家退出游戏
                commandExit(addr)  # 从user中删除此人
                continue

            else:
                if message != "":
                    logger.info(
                        "<{Account}> {Message}".format(
                            Account=users[addr]["account"], Message=message
                        )
                    )
        except Exception:
            logger.exception(":warning-emoji:  错误！", extra={"markup": True})


def newChoseCharacter(addr, message):
    id_max = len(data.getCharacters(addr))  # 最大ID设定(角色字典的长度即为个数)
    uuid = data.getUUID(addr)
    try:
        if message == "new":
            users[addr]["status"] = "createCharacter"  # 改状态为创建角色
            data.NewCharacterData[uuid] = {}  # 在新增用户中添加此UUID（不然会报IndexError）
            sock.sendto("006".encode("utf-8"), addr)
        elif int(message) > id_max:  # 输入ID超出范围
            sock.sendto("105".encode("utf-8"), addr)
        else:
            users[addr]["status"] = "online"  # 设置状态为在线
            users[addr]["characterName"] = data.getCharacterName(addr, int(message))
            users[addr]["characterID"] = int(message)  # 写入user中
            logger.info(
                "[yellow]<{Account}> 以 <{Character}> 角色进入了游戏...[/]".format(
                    Account=users[addr]["account"],
                    Character=users[addr]["characterName"],
                ),
                extra={"markup": True},
            )
            sock.sendto(
                "009{Name}".format(Name=users[addr]["characterName"]).encode("utf-8"),
                addr,
            )
            commandMap(addr)
            commandLook(addr)
    except Exception:
        sock.sendto("105".encode("utf-8"), addr)


def newCreateCharacter(addr, message):
    uuid = users[addr]["uuid"]
    if not isAllChinese(message):  # 若不全是中文
        sock.sendto("104".encode('utf-8'), addr)
    else:
        users[addr]["status"] = "online"
        try:
            id_max = len(data.getCharacters(addr))  # 获取角色数量
        except KeyError:
            id_max = 0  # 若没有角色就设为0
        data.addNewCharacter(
            addr, id_max + 1, message
        )  # 设置该uuid下的第<角色数量+1>个角色名为message

        data.addNewCharacterAttribute(uuid, id_max + 1)
        data.addNewCharacterPosition(uuid, id_max + 1)
        users[addr]["characterName"] = message
        users[addr]["characterID"] = id_max + 1  # 添加到用户列表
        logger.info(
            "[yellow]<{Account}> 创建了 <{Character}> 角色并进入游戏...[/]".format(
                Account=users[addr]["account"],
                Character=users[addr]["characterName"],
            ),
            extra={"markup": True},
        )
        for address in users:  # 从user提取出addr
            sock.sendto(
                "203{Name}".format(Name=message).encode(), address
            )  # 广播xxx进游戏啦！
        sock.sendto("007".encode("utf-8"), addr)
        commandMap(addr)
        commandLook(addr)


def newReg(addr, message):
    if (
        "firstPassword" in users[addr] and users[addr]["firstPassword"] != message
    ):  # 若有密码且前后密码不一致
        del users[addr]["firstPassword"]  # 将密码重置为空
        sock.sendto("102".encode('utf-8'), addr)  # 发送错误码

    elif "firstPassword" in users[addr]:  # 前后密码一致，注册成功
        sock.sendto("005".encode('utf-8'), addr)
        users[addr]["password"] = users[addr]["firstPassword"]  # 若两次密码相同
        del users[addr]["firstPassword"]  # 删除第一次密码的缓存

        conn = connectDB()
        account = users[addr]["account"]
        password = users[addr]["password"]
        with conn.cursor() as cursor:
            cursor.execute(
                f"INSERT INTO users (account,password) VALUES ('{account}','{password}')"
            )
            conn.commit()
            cursor.execute(f"SELECT uuid FROM users WHERE account='{account}'")
            UUID = cursor.fetchone()["uuid"]
        users[addr]["uuid"] = UUID  # 写入user中

        data.addNewAccount(
            uuid=UUID, password=users[addr]["password"], name=users[addr]["account"]
        )  # 加入到用户字典
        closeDB(conn)

        users[addr]["status"] = "createCharacter"  # 状态设置为创建角色中
        data.addNewCharacter(addr=addr)  # 将两个角色字典都添加uuid键
        sock.sendto("006".encode("utf-8"), addr)  # 注册必定没有角色，创建一个

    else:
        users[addr]["firstPassword"] = message  # 保存第一次输入的密码
        sock.sendto("004".encode("utf-8"), addr)


def newlogin(addr, message):
    if message != users[addr]["password"]:  # 密码错误
        sock.sendto("101".encode('utf-8'), addr)

    else:
        sock.sendto("002".encode('utf-8'), addr)  # 发送登陆成功状态码
        uuid = users[addr]["uuid"]  # 获取UUID
        character_data = data.getAllCharacterData()
        if uuid not in character_data.keys():  # 若没有检测到角色
            users[addr]["status"] = "createCharacter"  # 设置状态为创建角色中
            data.addNewCharacter(addr=addr)  # 将两个角色字典都添加uuid键
            sock.sendto("006".encode('utf-8'), addr)  # 检测到没有角色，创建一个

        else:  # 检测到账户有角色，选择角色
            users[addr]["status"] = "choseCharacter"
            sock.sendto(
                "008{id}|{characterName}".format(
                    id=list(character_data[uuid].keys()),
                    characterName=list(character_data[uuid].values()),
                ).encode('utf-8'),
                addr,
            )


def newUser(addr, message):
    account_data = data.getAllAccountData()
    if message not in account_data:
        users[addr] = {
            "account": message,
            "status": "reg",
        }  # 向user写入账户名，状态设为reg
        logger.info(f"[yellow]<{message}> 正在尝试注册...[/]", extra={"markup": True})
        sock.sendto('003'.encode('utf-8'), addr)
    else:
        users[addr] = {
            "account": message,
            "status": "login",
            "password": account_data[message]["password"],
            "uuid": account_data[message]["uuid"],
        }  # 将密码，UUID之类的都写进user
        logger.info(f"[yellow]<{message}> 正在尝试登陆...[/]", extra={"markup": True})
        sock.sendto('001'.encode('utf-8'), addr)


def create_logger():
    logging.basicConfig(
        level="INFO",
        format="%(message)s",
        datefmt="[%X]",
        handlers=[RichHandler(rich_tracebacks=True)],
    )

    return logging.getLogger("rich")


# sourcery skip: dict-assign-update-to-union
if __name__ == '__main__':
    path = os.path.dirname(os.path.abspath(__file__))
    logger = create_logger()
    Settings = {}
    install(show_locals=True)
    console = Console()
    # logo = Figlet(font="lean", width=1000)
    # print(YELLOW + logo.renderText("Immortal") + NORMAL)

    try:
        Settings["addrIP"] = getConfig("Internet", "IP")  # 先尝试读取config
        Settings["addrPort"] = int(getConfig("Internet", "Port"))  # 端口
        Settings["dbHost"] = getConfig("Internet", "dbhost")  # 数据库IP
        Settings["dbPort"] = int(getConfig("Internet", "dbport"))  # 数据库端口
        Settings["dbUser"] = getConfig("Internet", "dbuser")  # 数据库用户
        Settings["dbPassword"] = getConfig("Internet", "dbpassword")  # 数据库密码
        Settings["dbType"] = getConfig("Internet", "dbtype")  # 数据库类型
        Settings["Db"] = getConfig("Internet", "db")  # 默认数据库
        addrr = (Settings["addrIP"], Settings["addrPort"])
        # getConfig返回值为[("section", "value"),...]
        # item为("section", "value")
        logger.info("已读取配置文件")
    except (configparser.NoSectionError, configparser.NoOptionError):  # 发现没有
        logger.info('请输入服务器IP:')  # 就获取输入
        Settings["addrIP"] = input('>')
        logger.info('请输入服务器端口:')
        Settings["addrPort"] = int(input('>'))
        logger.info('请输入数据库类型(MySQL/PostgreSQL)：')
        Settings["dbType"] = input(">")
        logger.info('请输入数据库IP(本地:localhost)：')
        Settings["dbHost"] = input('>')
        logger.info('请输入数据库端口：')
        Settings["dbPort"] = int(input('>'))
        logger.info('请输入数据库用户名:')
        Settings["dbUser"] = input('>')
        logger.info("请输入数据库密码: ")
        Settings["dbPassword"] = input('>')
        logger.info("请输入使用的数据库名: ")
        Settings["Db"] = input(">")
        addrr = (Settings["addrIP"], Settings["addrPort"])
        if input("是否保存配置？Y(是)/N(否)") == "Y":
            writeConfig(
                "Internet", ["IP", "Port"], [Settings["addrIP"], Settings["addrPort"]]
            )
            writeConfig(
                "Internet",
                ["dbtype", "dbHost", "dbPort", "dbUser", "dbPassword", "Db"],
                [
                    Settings["dbType"],
                    Settings["dbHost"],
                    Settings["dbPort"],
                    Settings["dbUser"],
                    Settings["dbPassword"],
                    Settings["Db"],
                ],
            )
        writeConfig(
            "Map", ["size", "default_dimension", "default_position"], [2, "hub", [0, 5]]
        )
    finally:
        Settings.update({item[0]: item[1] for item in getConfig("Map")})

    start = time.time()
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 创建socket对象

    logger.info(f'正在连接至SQL服务器[{Settings["dbHost"]}:{Settings["dbPort"]}]')

    conn = connectDB()  # 连接数据库

    logger.info("正在获取存档")

    data = Data(conn)

    try:
        users = {}  # 存放字典{地址 : {状态:"xxx", xx:xx, ...}}
        data.setup()
        logger.info("获取成功！")
    except pymysql.err.ProgrammingError as e:
        conn.close()
        conn = connectDB()
        error_type = eval(e.__str__())[0]
        logger.warning("检测到有表缺失，是否修复结构(不会覆盖已创建的表)?Y/N")
        if input(">") == "Y" and error_type == 1146:
            with conn.cursor() as cursor:
                logger.info("数据库类型:MySQL")
                logger.info("正在创建users...")
                cursor.execute(
                    """
                CREATE TABLE if not exists `users` (
                    `uuid` int NOT NULL AUTO_INCREMENT,
                    `account` varchar(255) NOT NULL,
                    `password` varchar(255) NOT NULL,
                    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    `login_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (`uuid`)
                ) ENGINE=InnoDB AUTO_INCREMENT=10000000 DEFAULT CHARSET=utf8mb3"""
                )
                cursor.execute(
                    """
                SELECT
                    AUTO_INCREMENT
                FROM
                    INFORMATION_SCHEMA. TABLES
                WHERE
                    TABLE_NAME = 'users'"""
                )  # 注：MySQL创建递增后需要先读取一次
                logger.info("正在创建attribute...")
                cursor.execute(
                    """
                CREATE TABLE if not exists `attribute` (
                    `uuid` int NOT NULL,
                    `character_id` int NOT NULL,
                    `hp_max` int NOT NULL DEFAULT '200' COMMENT '生命值上限',
                    `hp` int NOT NULL DEFAULT '200' COMMENT '生命值',
                    `atk` int NOT NULL DEFAULT '10' COMMENT '攻击力',
                    `def` int NOT NULL DEFAULT '10' COMMENT '防御力',
                    `crit` int NOT NULL DEFAULT '1' COMMENT '暴击率',
                    `crit_damage` int NOT NULL DEFAULT '50' COMMENT '暴击伤害',
                    `dex` int NOT NULL DEFAULT '90' COMMENT '命中率',
                    `pot` int NOT NULL DEFAULT '0' COMMENT '潜力',
                    `lv` int NOT NULL DEFAULT '0' COMMENT '等级',
                    `exp` int NOT NULL DEFAULT '0' COMMENT '经验',
                    `exp_max` int NOT NULL DEFAULT '10' COMMENT '最大经验值',
                    `coin` int NOT NULL DEFAULT '500' COMMENT '钱'
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"""
                )
                logger.info("正在创建character...")
                cursor.execute(
                    """
                CREATE TABLE if not exists `character` (
                    `uuid` int NOT NULL,
                    `character_id` int NOT NULL,
                    `character_name` longtext NOT NULL,
                    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"""
                )
                logger.info("正在创建position...")
                cursor.execute(
                    """
                CREATE TABLE if not exists `position` (
                    `uuid` int NOT NULL,
                    `character_id` int NOT NULL,
                    `dimension` longtext NOT NULL,
                    `x` int NOT NULL,
                    `y` int NOT NULL
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"""
                )
                conn.commit()
                logger.info("创建成功")
                users = {}  # 存放字典{地址 : {状态:"xxx", xx:xx, ...}}
    except psycopg2.errors.UndefinedTable:
        conn.close()
        conn = connectDB()
        logger.warning("检测到有表缺失，是否修复结构(不会覆盖已创建的表)?Y/N")
        if input(">") == "Y":
            with conn.cursor() as cursor:
                logger.info("数据库类型:PostgreSQL")
                logger.info("正在创建user_seq...")
                cursor.execute(
                    """
                CREATE sequence if not exists "user_seq"
                    increment by 1 
                    minvalue 1 
                    no maxvalue 
                    start with 10000000;"""
                )
                logger.info("正在创建users...")
                cursor.execute(
                    """
                CREATE TABLE if not exists public.users (
                    uuid integer DEFAULT nextval('user_seq'::regclass) NOT NULL,
                    account character varying(255)  NOT NULL,
                    password character varying(255)  NOT NULL,
                    create_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP(0) NOT NULL,
                    login_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP(0) NOT NULL);"""
                )
                logger.info("正在创建attribute...")
                cursor.execute(
                    """
                CREATE TABLE if not exists public.attribute (
                    uuid integer  NOT NULL,
                    character_id integer  NOT NULL,
                    hp_max integer DEFAULT 200 NOT NULL,
                    hp integer DEFAULT 200 NOT NULL,
                    atk integer DEFAULT 10 NOT NULL,
                    def integer DEFAULT 10 NOT NULL,
                    crit integer DEFAULT 1 NOT NULL,
                    crit_damage integer DEFAULT 50 NOT NULL,
                    dex integer DEFAULT 90 NOT NULL,
                    pot integer DEFAULT 0 NOT NULL,
                    lv integer DEFAULT 1 NOT NULL,
                    exp integer DEFAULT 0 NOT NULL,
                    exp_max integer DEFAULT 10 NOT NULL,
                    coin integer DEFAULT 500 NOT NULL);"""
                )
                logger.info("正在创建character...")
                cursor.execute(
                    """
                CREATE TABLE if not exists public.character (
                    uuid integer  NOT NULL,
                    character_id integer  NOT NULL,
                    character_name character varying(255)  NOT NULL,
                    create_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP(0) NOT NULL);"""
                )
                logger.info("正在创建position...")
                cursor.execute(
                    """
                CREATE TABLE if not exists public.position (
                    uuid integer  NOT NULL,
                    character_id integer  NOT NULL,
                    dimension character varying(255)  NOT NULL,
                    x integer  NOT NULL,
                    y integer  NOT NULL);"""
                )
                conn.commit()
                logger.info("创建成功")
                users = {}  # 存放字典{地址 : {状态:"xxx", xx:xx, ...}}

    logger.info(
        "正在绑定至端口[{IP}:{Port}]".format(IP=Settings["addrIP"], Port=Settings["addrPort"])
    )

    sock.bind(addrr)  # 绑定地址和端口

    logger.info("绑定成功！")

    end = time.time()
    cost_time = end - start

    MainInThread = threading.Thread(target=Server_Input, args=(), name='MainIn')
    MainOutThread = threading.Thread(
        target=Client_Input, args=(), name='MainOut', daemon=True
    )
    AutoFunctionsThread = threading.Thread(
        target=auto_functions, args=(), name='AutoFunctions', daemon=True
    )

    atexit.register(shutDown, sock, conn)
    MainInThread.start()
    MainOutThread.start()
    AutoFunctionsThread.start()
    console.rule(f"启动成功 耗时{cost_time*1000:.0f}ms 地址：[{addrr[0]}:{addrr[1]}]")
    MainInThread.join()
    exit()
